package com.hzb.LNReader.convertor;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;

public class HtmlParser {

	public HtmlHandler handler;
	
	public HtmlParser(HtmlHandler handler) throws Exception {
		this(handler, "GBK");
	}
	
	public HtmlParser(HtmlHandler handler, String encoding) throws Exception {
		this.handler = handler;
		setEncoding(encoding);
	}
	
	InputStream in = null;
	byte[] buffer;
	int bufferHead = 0;
	int bufferEnd = 0;
	static final int bufferSize = 256 * 1024;
	
	static final byte st = (byte) '<',
			et = (byte) '>',
			eq = (byte) '=',
			qu = (byte) '\"',
			star = (byte) '*',
			m0 = (byte) '!',
			m1 = (byte) '-',
			splash = (byte) '/';
	
	private int fillBuffer() throws IOException {
		int size = bufferEnd - bufferHead;
		if (size == buffer.length) return 0;
		if (bufferHead != 0) {
			System.arraycopy(buffer, bufferHead, buffer, 0, size);
		}
		bufferEnd = size;
		bufferHead = 0;
		size = buffer.length - size;
		bufferEnd += (size = in.read(buffer, bufferEnd, size));
		return size;
	}
	
	public void setEncoding(String encoding) throws Exception {
		this.encoding = encoding;
	}
	
	private String encoding = "GBK";
	private HashMap<String, String> attrMap = new HashMap<String, String>();
	
	/**
	 * 结尾不包含
	 * @param start
	 * @param end 不包含
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	String newString(int start, int end) throws UnsupportedEncodingException {
		return new String(buffer, start, end - start, encoding);
	}
	
	String readToken() throws UnsupportedEncodingException {
		int kHead = -1, kEnd = -1;
		int i;
		for (i=iterHead; i!=iterEnd; i++) {
			if (i == bufferSize) i = 0;
			if (kHead == -1) {
//				遇到有效字符前
				if (Character.isWhitespace(buffer[i])) {
//					还是遇到空格
				} else {
//					遇到首个有效字符
					kHead = i;
				}
			} else {
//				有效字符中
				if (Character.isWhitespace(buffer[i])) {
//					遇到结束空白字符
					kEnd = i;
					break;
				} else {
//					遇到中间有效字符
				}
			}
		}
		iterHead = i;
		if (kHead == -1) return null;
		if (kEnd == -1) return newString(kHead, i);
		return newString(kHead, kEnd);
	}
	
	String readKey() throws UnsupportedEncodingException {
		int kHead = -1, kEnd = -1;
		boolean inSp = true;
		int i;
		for (i=iterHead; i!=iterEnd; i++) {
			if (i == bufferSize) i = 0;
			if (Character.isWhitespace(buffer[i])) {
//				遇到空格
				if (inSp) {
//					遇到连续的空格
				} else {
//					第一次遇到空格
					if (kHead != -1) kEnd = i;
					inSp = true;
				}
			} else {
//				遇到非空格
				if (buffer[i] == eq) {
//					遇到=
					if (kHead == -1) {
//						=前没有有效key
						
					} else {
//						有有效key
						if (kEnd == -1) kEnd = i; 
						break;
					}
				} else {
//					遇到非=
					if (inSp) {
//						遇到第一个有效字符
						kHead = i;
						kEnd = -1;
						inSp = false;
					} else {
//						遇到有效字符中间的东西
					}
				}
				inSp = false;
			}
		}
		iterHead = i;
		if (kHead == -1) return null;
		if (kEnd != -1) {
			iterHead++;
			return newString(kHead, kEnd);
		} else {
			return newString(kHead, i);
		}
	}
	
	String readValue() throws UnsupportedEncodingException {
		int kHead = -1, kEnd = -1;
		boolean inQu = false;
		int i;
		for (i=iterHead; i!=iterEnd; i++) {
			if (i == bufferSize) i = 0;
			if (!inQu) {
//				在value开始前
				if (buffer[i] == qu) {
//					遇到第一个"
					inQu = true;
					kHead = i;
				} else if (Character.isWhitespace(buffer[i])){
//					前缀空白字符
				} else {
//					遇到不正确value
					break;
				}
			} else {
//				在value开始后
				if (buffer[i] == qu) {
//					遇到value终结
					kEnd = i;
					break;
				} else {
//					遇到value中间字符
				}
			}
		}
		iterHead = i;
		if (kEnd != -1) {
			iterHead++;
			String ret = newString(kHead, kEnd);
			return ret.substring(1);
		}
		return null;
	}
	
	int iterHead, iterEnd;
	/**
	 * 解析一个Element的标记
	 * @param start 开始下标，'<'下一个位置
	 * @param end 结束下标,'>'所在位置
	 * @throws Exception 
	 */
	void parseElement(int start, int end) throws Exception {
		iterHead = start;
		iterEnd = end;
		
		String tag = readToken();
		if (tag == null) return ;
		if (tag.charAt(0) == splash) {
			tag = tag.substring(1).trim();
			if (tag.length() != 0 && !noEndTag.contains(tag)) handler.endElement(tag);
		} else {
			attrMap.clear();
			boolean selfEnd = false;
			for (;;) {
				String key = readKey();
				if (key == null) break;
				if ("/".equals(key)) {
					if (iterHead == iterEnd) {
						selfEnd = true;
						break;
					}
					else continue;
				}
				String value = readValue();
				if (value != null) {
					attrMap.put(key.toLowerCase(), value);
				}
			}
			if ("meta".equalsIgnoreCase(tag)) {
				if ("content-type".equalsIgnoreCase(attrMap.get("http-equiv"))) {
					String tmpStr = attrMap.get("content");
					if (tmpStr != null) {
						tmpStr = tmpStr.toLowerCase();
						int index = tmpStr.indexOf("charset");
						if (index != -1) {
							index = tmpStr.indexOf('=', index);
							int endIndex = tmpStr.indexOf(';', index);
							if (endIndex == -1) endIndex = tmpStr.length();
							tmpStr = tmpStr.substring(index + 1, endIndex).trim();
							if (tmpStr.contains("utf") && tmpStr.contains("8")) setEncoding("utf-8");
							else if (tmpStr.contains("gbk") || tmpStr.contains("gb2312")) setEncoding("gbk");
							else if (tmpStr.contains("big") && tmpStr.contains("5")) setEncoding("big5");
							else if (tmpStr.contains("iso") && tmpStr.contains("8859") && tmpStr.contains("1")) setEncoding("ISO8859-1");
						}
					}
				}
			}
			handler.startElement(tag, attrMap);
			if (selfEnd || noEndTag.contains(tag)) handler.endElement(tag);
		}
	}
	
	static final HashSet<String> noEndTag = new HashSet<String> (Arrays.asList("img", "br", "link", "meta"));
	
	public void parse(InputStream in) throws Exception {
		this.in = in;
		buffer = new byte[bufferSize];
		try {
			doParse();
		} finally {
			buffer = null;
			attrMap = null;
			in = null;
			handler.finish();
		}
	}
	private void doParse() throws IOException, Exception {
		boolean inIgnore = false;
		int pendingLen = 0;
		boolean ignoreMatch = false;
		
		for (int i=0; ;i++) {
			if (i == bufferEnd) {
				int rb = fillBuffer();
				if (0 >= rb) break;
				i = bufferEnd - rb;
			}
			if (inIgnore) {
//				忽略模式中
				switch (pendingLen) {
				case 0:
				case 1:
					if (buffer[i] == m1) {
						pendingLen++;
					} else {
						pendingLen = 0;
						bufferHead = i;
					}
					break;
				case 2:
					if (buffer[i] == et) {
						// 遇到忽略结尾
						inIgnore = false;
						bufferHead = i + 1;
					} else if (buffer[i] == m1){
						pendingLen = 1;
						bufferHead = i - 1;
					} else {
						pendingLen = 0;
						bufferHead = i + 1;
					}
					break;
				}
			} else {
//				非忽略模式中
				if (ignoreMatch) {
					pendingLen ++;
					switch (pendingLen) {
					case 2:
						if (buffer[i] != m0) ignoreMatch = false;
						break;
					case 3:
						if (buffer[i] != m1) ignoreMatch = false;
						break;
					case 4:
						ignoreMatch = false;
						if (buffer[i] == m1) {
							inIgnore = true;
							pendingLen = 0;
							bufferHead = i + 1;
							continue;
						}
						break;
					}
				}
				if (buffer[i] == st) {
//					遇到'<'
					if (bufferHead != i) {
						handler.characters(newString(bufferHead, i));
					}
					bufferHead = i;
					pendingLen = 1;
					ignoreMatch = true;
				} else if (buffer[i] == et) {
//					遇到'>'
					if (buffer[bufferHead] == st) {
//						前面有正常的'<'
						handler.endCharacters();
						parseElement(bufferHead + 1, i);
						bufferHead = i + 1;
						pendingLen = 0;
					}
				} else {
//					遇到非'<','>'
				}
			}
		}
	}
	
	public static void main(String[] args) throws Exception {
		new HtmlParser(new HtmlToTxtConvertor(new File("d:\\1.lnd"))).parse(new FileInputStream(new File("d:\\3.htm")));
	}
}
